package com.llbnk.controller;

import com.google.common.util.concurrent.RateLimiter;
import com.llbnk.error.BusinessException;
import com.llbnk.error.EmBusinessError;
import com.llbnk.log.LogAction;
import com.llbnk.log.LogGenerator;
import com.llbnk.logpool.LogPoolManager;
import com.llbnk.mq.MqProducer;
import com.llbnk.pojo.entity.DetailLog;
import com.llbnk.pojo.model.UserModel;
import com.llbnk.response.CommonReturnType;
import com.llbnk.service.ItemService;
import com.llbnk.service.OrderService;
import com.llbnk.service.PromoService;
import com.llbnk.util.CodeUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.*;

/**
 * @author shkstart
 * @create 2022-08-22 10:41
 */
@RestController
@CrossOrigin(allowCredentials = "true",allowedHeaders = "*")
@Slf4j
public class OrderController extends BaseController {

    //请求头Content-Type  application/x-www-form-urlencoded
    public static final String CONTENT_TYPE_FORMED="application/x-www-form-urlencoded";


    //异步更新日志
    @Resource
    private LogPoolManager logPoolManager;

    @Autowired
    private OrderService orderService;

    @Autowired
    private HttpServletRequest httpServletRequest;

    //用于作MQ事务处理
    @Resource
    private MqProducer mqProducer;

    @Resource
    private ItemService itemService;

    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    private PromoService promoService;

    private ExecutorService executorService;

    /**令牌桶算法用于限流*/
    private RateLimiter orderCreateRateLimiter;

    /**为什么要搞这个线程池？
     * */
    @PostConstruct
    public void init(){
        executorService = Executors.newFixedThreadPool(20);

        //每秒钟中限制流量位300
        //互联网对流量的优化分三步，限流、降级、熔断
        //guava参考文章：https://zhuanlan.zhihu.com/p/60979444
        orderCreateRateLimiter =RateLimiter.create(300);
    }

    //生成验证码
    @RequestMapping(value = "/order/generateverifycode",method = {RequestMethod.GET,RequestMethod.POST})
    @ResponseBody
    public void generateverifycode(HttpServletResponse response) throws BusinessException, IOException {

        //根据token获取用户信息
        String token = httpServletRequest.getParameterMap().get("token")[0];
        if (StringUtils.isEmpty(token)) {
            throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "用户还未登陆，不能生成验证码");
        }
        UserModel userModel = (UserModel) redisTemplate.opsForValue().get(token);
        if(userModel == null)
            throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "用户还未登陆，不能生成验证码");


        Map<String, Object> map = CodeUtil.generateCodeAndPic();
        redisTemplate.opsForValue().set("verify_code_"+userModel.getId(),map.get("code"));
        redisTemplate.expire("verify_code_"+userModel.getId(),10,TimeUnit.MINUTES);
        log.info("验证码的key-value： "+"verify_code_"+userModel.getId()+"  "+map.get("code"));
        //在生成验证码图片我们就不写在在本地磁盘里面了，而是写在返回给前端的httpresponse当中去
        ImageIO.write((RenderedImage) map.get("codePic"), "jpeg", response.getOutputStream());
    }


    //生成秒杀令牌
    @RequestMapping(value = "/order/generatetoken",method = {RequestMethod.POST},consumes={CONTENT_TYPE_FORMED})
    @ResponseBody
    //@RequestParam(name = "verifyCode") String verifyCode
    public CommonReturnType generatetoken(@RequestParam(name="itemId")Integer itemId,
                                          @RequestParam(name="promoId")Integer promoId) throws BusinessException {
        //根据token获取用户信息
        String token = httpServletRequest.getParameterMap().get("token")[0];
        if(StringUtils.isEmpty(token)){
            throw new BusinessException(EmBusinessError.USER_NOT_LOGIN,"用户还未登陆，不能下单");
        }

        //获取用户的登陆信息
        UserModel userModel = (UserModel) redisTemplate.opsForValue().get(token);
        if(userModel == null){
            throw new BusinessException(EmBusinessError.USER_NOT_LOGIN,"用户还未登陆，不能下单");
        }

//        //通过verifyCode验证验证码的有效性
//        String redisVerifyCode = (String) redisTemplate.opsForValue().get("verify_code_"+userModel.getId());
//        if(StringUtils.isEmpty(redisVerifyCode)){
//            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"请求非法");
//        }
//        if(!redisVerifyCode.equalsIgnoreCase(verifyCode)){
//            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"请求非法，验证码错误");
//        }

        //获取秒杀访问令牌
        String promoToken = promoService.generateSecondKillToken(promoId,itemId,userModel.getId());

        if(promoToken == null){
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"生成令牌失败");
        }

        //插入异步日志
        userLog(userModel.getId()+"", LogAction.FIND.getAction(),"用户下单前获取秒杀令牌");

        //返回对应的结果
        return CommonReturnType.create(promoToken);
    }

    //订单创建
    @PostMapping(value = "/order/createOrder", consumes = {CONTENT_TYPE_FORMED})
    public CommonReturnType createItem(@RequestParam(name = "itemId")Integer itemId,
                                       @RequestParam(name = "amount")Integer amount,
                                       @RequestParam(name = "promoId",required = false)Integer promoId,
                                       @RequestParam(name = "promoToken", required = false) String promoToken) throws BusinessException {
        //采用分布式登录会话机制检验token
        UserModel userModel = checkTokenAndUserInfo();

        /**简单用户下单令牌的操作，只有拿到令牌才能下单*/
        checkSecondsKillTheToken(promoId,userModel,itemId,promoToken);

        /*最核心方法，包括
        1.由线程池组建的泄洪闸门
        2.设置异步操作流水
        3.利用rokcetMQ事务，保证redis，mysql的最终一致性*/
        theCoreLogic(itemId,amount,userModel,promoId);


        //插入异步日志
        userLog(userModel.getId()+"", LogAction.INCREASE.getAction(),"用户下单预约"+itemId+"活动"+amount+"人");


        return CommonReturnType.create(null);
    }

    private void theCoreLogic(Integer itemId, Integer amount, UserModel userModel, Integer promoId) throws BusinessException {

        //若库存不足直接返回下单失败
        //(2)问题又来了，假如说我们当前商品只有100个，但我们有1w个人点击了在OrderController中
        //设置插入数据库，此时就会造成生成了1w个流水信息，产生了极大的浪费
        /*注意这条已经放入在PromoServiceImpl的generateSecondKillToken方法中，
         * 认为先判断是否库存售空，然后再进行发放令牌操作*/
//        if(redisTemplate.hasKey("promo_item_stock_invalid_"+itemId)){
//            throw new BusinessException(EmBusinessError.STOCK_NOT_ENOUGH);
//        }


        //同步调用线程池的submit方法
        //future类讲的非常形象：https://blog.csdn.net/qq_35190492/article/details/115609360?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522164153878016780261918176%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=164153878016780261918176&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-12-115609360.nonecase&utm_term=%E6%A8%A1%E5%BC%8F&spm=1018.2226.3001.4450
        //我们首先搞了一个20个大小的线程池
        //这个线程池实现callable方法，其实就是搞了一个大小为20的等待队列用来队列化泄洪
        //也就是说在一台服务器上只有20个请求能过来下单，剩下的请求全部要过来排队
        /*其实就是这个线程池只有20各大小，如果当前请求数量大于20就要将多余的线程数量放入线程池的阻塞队列中进行排队处理*/
        Future<Object> future = executorService.submit(new Callable<Object>() {
            @Override
            public Object call() throws Exception {

                //为了根据checkLocalTransaction确定消息的状态，需要引入操作流水（操作型数据：log data）
                //他的作用就是先插入一条异步流水，根据这个status用来追踪异步扣减库存这个消息。
                //就是根据你这个流水的状态来确定MQ返回是成功还是失败还是不知道
                String stockLogId = itemService.initStockLog(itemId, amount);

                //OrderModel orderModel = orderService.createOrder(userModel.getId(), itemId, promoId, amount);
                //因为需要保证MQ中信息发送必须成功，所以采用rocketMQ事务
                boolean result = mqProducer.transactionAsyncReduceStock(userModel.getId(), itemId,
                        promoId, amount,stockLogId);
                if(!result){
                    throw new BusinessException(EmBusinessError.UNKNOW_ERROR,"下单失败");
                }
                return null;
            }
        });

        try {
            future.get();
        } catch (InterruptedException e) {
            throw new BusinessException(EmBusinessError.UNKNOW_ERROR);
        } catch (ExecutionException e) {
            throw new BusinessException(EmBusinessError.UNKNOW_ERROR);
        }

    }


    /**简单用户下单令牌的操作，只有拿到令牌才能下单*/
    private void checkSecondsKillTheToken(Integer promoId, UserModel userModel, Integer itemId, String promoToken) throws BusinessException {

        if(promoId!=null){
            String inRedisPromoToken = (String)redisTemplate
                    .opsForValue().get("promo_token_"+promoId+"_userid_"+userModel.getId()+"_itemid_"+itemId);
            if(inRedisPromoToken == null){
                throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"秒杀令牌校验失败");
            }
            if(!StringUtils.equals(promoToken,inRedisPromoToken)){
                throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"秒杀令牌校验失败");
            }
        }
    }


    private UserModel checkTokenAndUserInfo() throws BusinessException {

        //本地限流方式使用guava的令牌桶限流每秒钟处理300个请求
        if(!orderCreateRateLimiter.tryAcquire()){
            throw new BusinessException(EmBusinessError.RATELIMIT);
        }

        //判断是否登录  本地方法从servlet的threadlocal中的session取出校验
//        Boolean isLogin = (Boolean) httpServletRequest.getSession().getAttribute("IS_LOGIN");
//        if(isLogin == null || !isLogin){
//            throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "请登录后再进行预约");
//        }
//        //获取用户的登录信息
//        UserModel userModel = (UserModel) httpServletRequest.getSession().getAttribute("LOGIN_USER");


        //采取新的放法 分布式会话将token放入redis中
        String token = httpServletRequest.getParameterMap().get("token")[0];
        if(StringUtils.isEmpty(token)){
            throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "请登录后再进行预约");
        }

        //验证用户登录是否过期
        UserModel userModel = (UserModel) redisTemplate.opsForValue().get(token);
        if(userModel == null){
            throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "用户还未登录，不能下单");
        }
        
        return userModel;
    }


    /**专门用于插入异步日志的方法*/
    public void userLog(String userId,String action,String info){
        DetailLog DetailLog = new DetailLog();
        DetailLog.setLogcontent(LogGenerator.getLog(httpServletRequest,userId,action, info));
        // 将业务日志放入到队列中，然后使用线程 异步 批量进行入库，以提升接口的响应速度
        try {
            logPoolManager.addLog(DetailLog);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }

}
